package com.zyf.project.capchain.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zyf.project.capchain.config.StrategyConfig;
import com.zyf.project.capchain.dao.UserDAO;
import com.zyf.project.capchain.dto.UserDTO;
import com.zyf.project.capchain.exceptions.EmptyParamsException;
import com.zyf.project.capchain.exceptions.OpenidException;
import com.zyf.project.capchain.exceptions.SqlErrorException;
import com.zyf.project.capchain.exceptions.WxUserNotExistException;
import com.zyf.project.capchain.listener.result.impl.Result;
import com.zyf.project.capchain.model.User;
import com.zyf.project.capchain.service.WxUserInfoService;
import com.zyf.project.capchain.utils.common.JwtUtils;
import com.zyf.project.capchain.utils.constant.RedisKeyConstant;
import com.zyf.project.capchain.utils.cryptology.EncryptUtils;
import com.zyf.project.capchain.listener.result.template.ResultTemplate;
import com.zyf.project.capchain.utils.wx.WxLoginUtils;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.HashMap;
import java.util.Map;

/**
 * <h1>微信用户基本信息服务实现</h1>
 *
 * @author Zhou Yifan
 */
@Service
@RequiredArgsConstructor
public class WxUserInfoServiceImpl implements WxUserInfoService {

    private final UserDAO userDAO;

    private final JwtUtils jwtUtils;

    private final ResultTemplate<User> userResultTemplate;

    private final RedisTemplate<String, UserDTO> redisTemplate;

    private final StrategyConfig strategyConfig;

    /**
     * 登录认证
     *
     * @param code 小程序传来的 code
     * @return 处理结果
     */
    @Override
    public Result<User> authorizeLogin(String code) {
        Result<User> result = new Result<>();

        // 入参不可以为空
        if (code == null || code.isEmpty()) {
            throw new EmptyParamsException("code不存在或为空");
        }
        // 获取认证参数
        Map<String, String> map = getAuthFromCode(code);
        if (map.containsKey("noOpenid")) {
            throw new OpenidException("未检测到 openid");
        }
        User user;
        // 根据认证参数以及策略模式查询用户
        if (Boolean.TRUE.equals(strategyConfig.getUSE_REDIS_IN_LOGIN())) {
            user = loginAndCheckUserFromRedisAndMysql(map);
        } else {
            user = loginAndCheckUserFromMysql(map);
        }
        // 登陆成功
        userResultTemplate.wxLoginSuccess(result, user);
        return result;
    }

    /**
     * 更新用户信息
     *
     * @param user 用户对象
     * @return 处理结果
     */
    @Override
    public Result<User> updateUser(User user) {
        Result<User> result = new Result<>();
        User newUser;
        // 入参不可以为空
        if (user == null || user.isLogicalEmpty() || Boolean.TRUE.equals(user.isAuthEmpty())) {
            throw new EmptyParamsException("user 不存在或缺少必要参数：uid 或 openid 或 token 或 sessionKey");
        }
        // 更新用户
        if (Boolean.TRUE.equals(strategyConfig.getUSE_REDIS_IN_LOGIN())) {
            newUser = updateUserFromRedisAndMysql(user);
        } else {
            newUser = updateUserFromMysql(user);
        }
        // 操作成功
        userResultTemplate.success(result, newUser);
        return result;
    }

    /**
     * 根据认证参数查询并检查用户，不过如果访问量不高，建议不使用 Redis，而是 {@link #loginAndCheckUserFromMysql}
     *
     * @param map 认证参数
     * @return 用户对象
     */
    private @NotNull User loginAndCheckUserFromRedisAndMysql(@NotNull Map<String, String> map) {
        // 获取必要参数
        String openid = map.get("openid");
        String sessionKey = map.get("sessionKey");
        String token = map.get("token");

        User user = new User();
        // 在缓存中查询
        UserDTO userDtoFromRedis = (UserDTO) redisTemplate.opsForHash().get(RedisKeyConstant.USER_LOGIN_AND_SIGNUP_SUCCESS.getKey(), openid);
        if (userDtoFromRedis != null) {
            // 如果缓存里面有，那么就说明是老用户，且不用在数据库里面找了
            user = userDtoFromRedis.toModel();
            user.setIsOld(true);
            user.setToken(token);
            return user;
        }
        // 如果缓存里面没有，那么就在数据库里面找
        UserDTO userDtoFromMysql = userDAO.selectByOpenid(openid);
        if (userDtoFromMysql != null) {
            // 如果找到了，就是老用户
            user = userDtoFromMysql.toModel();
            user.setIsOld(true);
        } else {
            // 如果查不到，说明是新用户，前端需要进行可能的新用户设置，后端将新的 user 插入到数据库中
            user.setIsOld(false);
            user.setOpenid(openid);
            user.setSessionKey(sessionKey);
            int res = userDAO.insert(user.toDTO());
            if (res < 1) {
                throw new SqlErrorException("数据层执行错误，发生语句为：userDAO.insert(user.toDTO())");
            }
        }
        // 新的 user 要存入到缓存中，安全起见，需不包含 token
        redisTemplate.opsForHash().put(RedisKeyConstant.USER_LOGIN_AND_SIGNUP_SUCCESS.getKey(), openid, user.toDTO());
        // 设置token
        user.setToken(token);
        return user;
    }

    /**
     * 根据认证参数查询并检查用户，不过如果访问量很高，建议使用 Redis，即 {@link #loginAndCheckUserFromRedisAndMysql}
     *
     * @param map 认证参数
     * @return 用户对象
     */
    private @NotNull User loginAndCheckUserFromMysql(@NotNull Map<String, String> map) {
        // 获取必要参数
        String openid = map.get("openid");
        String sessionKey = map.get("sessionKey");
        String token = map.get("token");

        User user = new User();
        // 在数据库里面找
        UserDTO userDtoFromMysql = userDAO.selectByOpenid(openid);
        if (userDtoFromMysql != null) {
            // 如果找到了，就是老用户
            user = userDtoFromMysql.toModel();
            user.setIsOld(true);
        } else {
            // 如果查不到，说明是新用户，前端需要进行可能的新用户设置，后端将新的 user 插入到数据库中
            user.setIsOld(false);
            user.setOpenid(openid);
            user.setSessionKey(sessionKey);
            int res = userDAO.insert(user.toDTO());
            if (res < 1) {
                throw new SqlErrorException("数据层执行错误，发生语句为：userDAO.insert(user.toDTO())");
            }
        }
        // 设置token
        user.setToken(token);
        return user;
    }

    /**
     * 更新用户，不过如果访问量不高，建议不使用 Redis，而是 {@link #updateUserFromMysql}
     *
     * @param user 用户对象
     * @return 用户对象
     */
    private @NotNull User updateUserFromRedisAndMysql(@NotNull User user) {
        UserDTO userDtoFromRedis = (UserDTO) redisTemplate.opsForHash().get(RedisKeyConstant.USER_LOGIN_AND_SIGNUP_SUCCESS.getKey(), user.getOpenid());
        if (userDtoFromRedis != null) {
            return userDtoFromRedis.toModel();
        }
        UserDTO userDtoFromMysql = userDAO.selectByOpenid(user.getOpenid());
        if (userDtoFromMysql == null) {
            throw new WxUserNotExistException("检测到微信用户不存在，发生语句为：userDAO.selectByOpenid(user.getOpenid())");
        }
        redisTemplate.opsForHash().put(RedisKeyConstant.USER_LOGIN_AND_SIGNUP_SUCCESS.getKey(), user.getOpenid(), user.toDTO());
        int res = userDAO.updateById(userDtoFromMysql);
        if (res < 1) {
            throw new SqlErrorException("数据层执行错误，发生语句为：userDAO.updateById(userDtoFromMysql)");
        }
        return userDtoFromMysql.toModel();
    }

    /**
     * 更新用户，不过如果访问量很高，建议使用 Redis，即 {@link #updateUserFromRedisAndMysql}
     *
     * @param user 用户对象
     * @return 用户对象
     */
    private @NotNull User updateUserFromMysql(@NotNull User user) {
        UserDTO userDtoFromMysql = userDAO.selectByOpenid(user.getOpenid());
        if (userDtoFromMysql == null) {
            throw new WxUserNotExistException("检测到微信用户不存在，发生语句为：userDAO.selectByOpenid(user.getOpenid())");
        }
        int res = userDAO.updateById(user.toDTO());
        if (res < 1) {
            throw new SqlErrorException("数据层执行错误，发生语句为：userDAO.updateById(userDtoFromMysql)");
        }
        return userDtoFromMysql.toModel();
    }

    /**
     * 根据 code 获取认证参数
     *
     * @param code 小程序传来的 code
     * @return 包括 openid、sessionKey、token
     */
    private @NotNull Map<String, String> getAuthFromCode(String code) {
        Map<String, String> map = new HashMap<>(4);
        JSONObject resultJson = WxLoginUtils.getResultJson(code);
        if (!resultJson.containsKey("openid")) {
            map.put("noOpenid", "true");
            return map;
        }
        String sessionKey = resultJson.getString("session_key");
        String openid = resultJson.getString("openid");
        String token = getToken(openid, sessionKey);
        map.put("openid", openid);
        map.put("sessionKey", sessionKey);
        map.put("token", token);
        return map;
    }

    /**
     * 根据 openid 以及 sessionKey 获取 token
     *
     * @param openid     openid
     * @param sessionKey session_key
     * @return token
     */
    private String getToken(String openid, String sessionKey) {
        // 生成 session 字符串，用于加密
        Map<String, String> sessionMap = new HashMap<>(2);
        sessionMap.put("sessionKey", sessionKey);
        sessionMap.put("openid", openid);
        String session = JSON.toJSONString(sessionMap);
        try {
            // 使用公钥加密对 session 加密
            PublicKey publicKey = EncryptUtils.getPublicKey();
            session = EncryptUtils.encryptRSA(session, publicKey);
        } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
        }
        // 生成token
        return jwtUtils.getToken(session);
    }
}
